/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
/*
 * twitter_api.c
 * Copyright (C) Jonas Hagmar 2009 <gnotterdev@gmail.com>
 * 
 * twitter_api.c is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * twitter_api.c is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#define _GNU_SOURCE
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <locale.h>
#include "twitter_api.h"

#define BUFFER_SIZE 256

static const char *HREF_PREFIX = "<a href=\"";

static char *
parse_source_href (const char *source)
{
  int const HREF_PREFIX_LEN = strlen (HREF_PREFIX);
  char const *start;
  char const *end;
  size_t href_len;
  char *result;

  if (strncmp (source, HREF_PREFIX, HREF_PREFIX_LEN) != 0)
    return NULL;

  end = start = source + HREF_PREFIX_LEN;
  while ((*end != 0) && (*end != '\"'))
    end++;
  href_len = end - start;
  result = (char *) malloc (href_len + 1);
  strncpy (result, start, href_len);
  result[href_len] = 0;
  return result;
}

static char *
parse_source_name (const char *source)
{
  int const HREF_PREFIX_LEN = strlen (HREF_PREFIX);
  char const *start;
  char const *end;
  size_t name_len;
  char *result;

  if (strncmp (source, HREF_PREFIX, HREF_PREFIX_LEN) == 0)
    {
      start = source + HREF_PREFIX_LEN;
      while ((*start != 0) && (*start != '>'))
	start++;
      if (*start == '>')
	start++;
      end = start;
      while ((*end != 0) && (*end != '<'))
	end++;
      name_len = end - start;
      result = (char *) malloc (name_len + 1);
      strncpy (result, start, name_len);
      result[name_len] = 0;
      return result;
    }
  else
    {
      name_len = strlen (source);
      result = (char *) malloc (name_len + 1);
      strncpy (result, source, name_len + 1);
      return result;
    }

}

static char *
parse_created_at (const char *created_at)
{
  struct tm *ta;
  struct tm post;
  int seconds_local;
  int seconds_post;
  int diff;
  char *oldenv;
  time_t t = time (NULL);
  char buf[BUFFER_SIZE];
  int result_len;
  char *result;

  tzset ();

  ta = gmtime (&t);
  ta->tm_isdst = -1;
  seconds_local = mktime (ta);

  oldenv = setlocale (LC_TIME, "C");
  strptime (created_at, "%a %b %d %T +0000 %Y", &post);
  post.tm_isdst = -1;
  seconds_post = mktime (&post);

  setlocale (LC_TIME, oldenv);

  diff = difftime (seconds_local, seconds_post);

  if (diff < 2)
    {
      snprintf (buf, BUFFER_SIZE, "about 1 second ago");
    }
  else
    /* Seconds */
  if (diff < 60)
    {
      snprintf (buf, BUFFER_SIZE, "about %i seconds ago", diff);
    }
  else if (diff < 120)
    {
      snprintf (buf, BUFFER_SIZE, "about 1 minute ago");
    }
  else
    {
      /* Minutes */
      diff = diff / 60;
      if (diff < 60)
	{
	  snprintf (buf, BUFFER_SIZE, "about %i minutes ago", diff);
	}
      else if (diff < 120)
	{
	  snprintf (buf, BUFFER_SIZE, "about 1 hour ago");
	}
      else
	{
	  /* Hours */
	  diff = diff / 60;
	  if (diff < 24)
	    {
	      snprintf (buf, BUFFER_SIZE, "about %i hours ago", diff);
	    }
	  else if (diff < 48)
	    {
	      snprintf (buf, BUFFER_SIZE, "about 1 day ago");
	    }
	  else
	    {
	      /* Days */
	      diff = diff / 24;
	      if (diff < 30)
		{
		  snprintf (buf, BUFFER_SIZE, "about %i days ago", diff);
		}
	      else if (diff < 60)
		{
		  snprintf (buf, BUFFER_SIZE, "about 1 month ago");
		}
	      else
		{
		  snprintf (buf, BUFFER_SIZE, "about %i months ago",
			    (diff / 30));
		}
	    }
	}
    }
  result_len = strlen (buf);
  result = (char *) malloc (result_len + 1);
  strcpy (result, buf);
  return result;
}

static void
add_status_element (twapi_status_list * list,
		    char const *user_screen_name,
		    char const *text,
		    char const *created_at,
		    char const *source,
		    char const *id, char const *user_profile_image_url)
{
  int user_screen_name_len;
  int text_len;
  int id_len;
  int user_profile_image_url_len;
  char *tmp;
  twapi_status_element_ptr e;
  twapi_status_element_ptr last;

  e = (twapi_status_element_ptr) malloc (sizeof (twapi_status_element));

  user_screen_name_len = strlen (user_screen_name);
  tmp = (char *) malloc (user_screen_name_len + 1);
  strcpy (tmp, user_screen_name);
  e->user_screen_name = tmp;

  text_len = strlen (text);
  tmp = (char *) malloc (text_len + 1);
  strcpy (tmp, text);
  e->text = tmp;

  e->created_at = parse_created_at (created_at);

  e->source_href = parse_source_href (source);

  e->source_name = parse_source_name (source);

  id_len = strlen (id);
  tmp = (char *) malloc (id_len + 1);
  strcpy (tmp, id);
  e->id = tmp;

  user_profile_image_url_len = strlen (user_profile_image_url);
  tmp = (char *) malloc (user_profile_image_url_len + 1);
  strcpy (tmp, user_profile_image_url);
  e->user_profile_image_url = tmp;

  e->next = NULL;

  if (!*list)
    {
      *list = e;
      return;
    }

  last = *list;
  while (last->next)
    last = last->next;

  last->next = e;

  return;
}

static xmlNode *
sibling (xmlNode * n)
{
  return n->next->next;
}

static xmlNode *
child (xmlNode * n, int i)
{
  n = n->children->next;
  while (i--)
    n = n->next->next;
  return n;
}

static char *
child_content (xmlNode * n, int i)
{
  n = n->children->next;
  while (i--)
    n = n->next->next;
  return (char *) n->children->content;
}

static void
build_status_list (xmlNode * a_node, twapi_status_list * list)
{
  xmlNode *status_node = NULL;

  for (status_node = child (a_node, 0); status_node;
       status_node = sibling (status_node))
    {
      add_status_element (list,
			  child_content (child (status_node, 9), 2),
			  child_content (status_node, 2),
			  child_content (status_node, 0),
			  child_content (status_node, 3),
			  child_content (status_node, 1),
			  child_content (child (status_node, 9), 5));
    }
}

/**
 * Free a list of status elements.
 * @param list the list to free
 */
void
twapi_status_list_free (twapi_status_list list)
{
  if (list)
    {
      twapi_status_list_free (list->next);
      free ((char *) list->user_screen_name);
      free ((char *) list->text);
      free ((char *) list->created_at);
      if (list->source_href)
	free ((char *) list->source_href);
      free ((char *) list->source_name);
      free ((char *) list->id);
      free ((char *) list->user_profile_image_url);
      free (list);
    }
}

/**
 * Update a Twitter status.
 * @param uname user name
 * @param passw password
 * @param status new status
 * @return TRUE if update was successful, FALSE if not
 */
int
twapi_update (char const *uname, char const *passw, char const *status,
	      char const *in_reply_to_status_id)
{

  CURL *curl;			/* CURL context */
  CURLcode res = 1;		/* CURL return code */
  char *uname_passw;		/* concatenated user name and password */
  struct curl_httppost *formpost = NULL;
  struct curl_httppost *lastptr = NULL;
  struct curl_slist *headerlist = NULL;
  char buf[] = "Expect:";
  int rv = FALSE;		/* return value */

  curl = curl_easy_init ();
  if (!curl)
    {
      goto curl_easy_init_fail;
    }

  /* allocate string to concatenate user name and password */
  uname_passw =
    malloc (sizeof (char) * (strlen (uname) + strlen (passw) + 2));
  if (!uname_passw)
    {
      goto uname_passw_malloc_fail;
    }

  /* concatenate user name and password */
  strcpy (uname_passw, uname);
  strcat (uname_passw, ":");
  strcat (uname_passw, passw);


  /* set form fields for POST */
  res = curl_formadd (&formpost,
		      &lastptr,
		      CURLFORM_COPYNAME, "status",
		      CURLFORM_COPYCONTENTS, status, CURLFORM_END);
  if (res != 0)
    {
      goto curl_formadd_fail;
    }

  if (in_reply_to_status_id != NULL)
    {
      res = curl_formadd (&formpost,
			  &lastptr,
			  CURLFORM_COPYNAME, "in_reply_to_status_id",
			  CURLFORM_COPYCONTENTS, in_reply_to_status_id,
			  CURLFORM_END);
      if (res != 0)
	{
	  goto curl_formadd_fail;
	}
    }

  /* initalize custom header list (stating that Expect: 100-continue is not
     wanted */
  headerlist = curl_slist_append (headerlist, buf);
  if (!headerlist)
    {
      goto curl_slist_append_fail;
    }

  /* set CURL options */
  if (curl_easy_setopt
      (curl, CURLOPT_URL,
       "https://twitter.com/statuses/update.xml") != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headerlist) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_HTTPPOST, formpost) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, FALSE) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_USERPWD, uname_passw) != CURLE_OK)
    goto curl_easy_setopt_fail;

  /* perform POST */
  res = curl_easy_perform (curl);

  if (res == 0)
    rv = TRUE;

curl_easy_setopt_fail:
  curl_slist_free_all (headerlist);
curl_slist_append_fail:
  curl_formfree (formpost);
curl_formadd_fail:
  /* free concatenated username and password */
  free (uname_passw);
uname_passw_malloc_fail:
  /* cleanup CURL context */
  curl_easy_cleanup (curl);
curl_easy_init_fail:
  return rv;
}

/**
 * Get the friends timeline from Twitter for the specified user.
 * @param uname user name
 * @param passw password
 * @return TRUE if success, FALSE if not
 */
int
twapi_friends_timeline (char const *uname, char const *passw,
			twapi_status_list * list)
{

  char *uname_passw;
  CURL *curl;
  CURLcode res;
  /*xmlDtdPtr dtd;*/
  /*xmlValidCtxt cvp;*/
  xmlParserCtxtPtr ctxt;
  xmlDocPtr doc;
  /*xmlParserInputBufferPtr dtd_buf;*/
  /*const char *dtd_text =
    "<!ELEMENT statuses (status+)>\n"
    "<!ATTLIST statuses type CDATA \"array\">\n"
    "<!ELEMENT status (created_at, id, text, source, truncated, in_reply_to_status_id, in_reply_to_user_id, favorited, in_reply_to_screen_name, user)>\n"
    "<!ELEMENT created_at (#PCDATA)>\n"
    "<!ELEMENT id (#PCDATA)>\n"
    "<!ELEMENT text (#PCDATA)>\n"
    "<!ELEMENT source (#PCDATA)>\n"
    "<!ELEMENT truncated (#PCDATA)>\n"
    "<!ELEMENT in_reply_to_status_id (#PCDATA)>\n"
    "<!ELEMENT in_reply_to_user_id (#PCDATA)>\n"
    "<!ELEMENT favorited (#PCDATA)>\n"
    "<!ELEMENT in_reply_to_screen_name (#PCDATA)>\n"
    "<!ELEMENT user (id, name, screen_name, location, description, profile_image_url, url, protected, followers_count, ((profile_background_color, profile_text_color, profile_link_color, profile_sidebar_fill_color, profile_sidebar_border_color, friends_count, created_at, favourites_count, utc_offset, time_zone, profile_background_image_url, profile_background_tile, statuses_count, notifications, following)?))>\n"
    "<!ELEMENT name (#PCDATA)>\n"
    "<!ELEMENT screen_name (#PCDATA)>\n"
    "<!ELEMENT location (#PCDATA)>\n"
    "<!ELEMENT description (#PCDATA)>\n"
    "<!ELEMENT profile_image_url (#PCDATA)>\n"
    "<!ELEMENT url (#PCDATA)>\n"
    "<!ELEMENT protected (#PCDATA)>\n"
    "<!ELEMENT followers_count (#PCDATA)>\n"
    "<!ELEMENT profile_background_color (#PCDATA)>\n"
    "<!ELEMENT profile_text_color (#PCDATA)>\n"
    "<!ELEMENT profile_link_color (#PCDATA)>\n"
    "<!ELEMENT profile_sidebar_fill_color (#PCDATA)>\n"
    "<!ELEMENT profile_sidebar_border_color (#PCDATA)>\n"
    "<!ELEMENT friends_count (#PCDATA)>\n"
    "<!ELEMENT favourites_count (#PCDATA)>\n"
    "<!ELEMENT utc_offset (#PCDATA)>\n"
    "<!ELEMENT time_zone (#PCDATA)>\n"
    "<!ELEMENT profile_background_image_url (#PCDATA)>\n"
    "<!ELEMENT profile_background_tile (#PCDATA)>\n"
    "<!ELEMENT statuses_count (#PCDATA)>\n"
    "<!ELEMENT notifications (#PCDATA)>\n" "<!ELEMENT following (#PCDATA)>\n";*/
  int rv = FALSE;		/* return value */

  size_t write_data (void *buffer, size_t size, size_t nmemb, void *userp)
  {
    size_t buffer_size = size * nmemb;
    xmlParseChunk (ctxt, buffer, buffer_size, 0);
    return buffer_size;
  }

  /* allocate string to concatenate user name and password */
  uname_passw =
    malloc (sizeof (char) * (strlen (uname) + strlen (passw) + 2));
  if (!uname_passw)
    {
      goto uname_passw_malloc_fail;
    }
  /* concatenate user name and password */
  strcpy (uname_passw, uname);
  strcat (uname_passw, ":");
  strcat (uname_passw, passw);

  curl = curl_easy_init ();
  if (!curl)
    {
      goto curl_easy_init_fail;
    }

  if (curl_easy_setopt
      (curl, CURLOPT_URL,
       "https://twitter.com/statuses/friends_timeline.xml") != CURLE_OK)
    goto curl_easy_setopt_fail;
  /*if (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE) != CURLE_OK) goto curl_easy_setopt_fail; */
  if (curl_easy_setopt (curl, CURLOPT_USERPWD, uname_passw) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_data) != CURLE_OK)
    goto curl_easy_setopt_fail;

  ctxt = xmlCreatePushParserCtxt (NULL, NULL, NULL, 0, NULL);
  if (ctxt == NULL)
    {
      goto xmlCreatePushParserCtxt_fail;
    }

  res = curl_easy_perform (curl);
  if (res != CURLE_OK)
    {
      goto curl_easy_perform_fail;
    }

  xmlParseChunk (ctxt, NULL, 0, 1);
  if (!ctxt->wellFormed)
    {
      goto xmlParseChunk_fail;
    }
  doc = ctxt->myDoc;

  /*dtd_buf =
    xmlParserInputBufferCreateMem (dtd_text, strlen (dtd_text) + 1,
				   XML_CHAR_ENCODING_ASCII);
  if (!dtd_buf)
    {
      goto xmlParserInputBufferCreateMem_fail;
    }

  dtd = xmlIOParseDTD (NULL, dtd_buf, XML_CHAR_ENCODING_ASCII);
  if (!dtd)
    {
      goto xmlIOParseDTD_fail;
    }

  cvp.userData = (void *) stderr;
  cvp.error = (xmlValidityErrorFunc) fprintf;
  cvp.warning = (xmlValidityWarningFunc) fprintf;

  if (!xmlValidateDtd (&cvp, doc, dtd))
    {
      goto xmlValidateDtd_fail;
    }*/

  rv = TRUE;
  *list = 0;
  build_status_list (xmlDocGetRootElement (doc), list);

/*xmlValidateDtd_fail:
  xmlFreeDtd (dtd);
xmlIOParseDTD_fail:
xmlParserInputBufferCreateMem_fail:
  xmlFreeDoc (doc);*/
xmlParseChunk_fail:
curl_easy_perform_fail:
  xmlFreeParserCtxt (ctxt);
xmlCreatePushParserCtxt_fail:
curl_easy_setopt_fail:
  curl_easy_cleanup (curl);
curl_easy_init_fail:
  free (uname_passw);
uname_passw_malloc_fail:
  return rv;
}

int 
twapi_http_get_to_file (char const *url, char const *filename) 
{
  CURL * curl;
  CURLcode res;
  FILE * f;
  int rv = FALSE;
  curl = curl_easy_init ();
  if (!curl)
    {
      goto curl_easy_init_fail;
    }

  f = fopen (filename, "w");
  if (f == NULL)
    goto fopen_fail;

  if (curl_easy_setopt (curl, CURLOPT_URL, url) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, NULL) != CURLE_OK)
    goto curl_easy_setopt_fail;
  if (curl_easy_setopt (curl, CURLOPT_WRITEDATA, f) != CURLE_OK)
    goto curl_easy_setopt_fail;
  res = curl_easy_perform (curl);
  if (res != CURLE_OK)
    goto curl_easy_perform_fail;

  rv = TRUE;

curl_easy_perform_fail:
curl_easy_setopt_fail:
  fclose (f);
fopen_fail:curl_easy_cleanup (curl);
curl_easy_init_fail:return rv;
}

#define HTTP_OK 200

char *
twapi_shorten_url (char const *url) 
{
  CURL * curl;
  CURLcode res;
  long rc;
  size_t rv_size = 0;
  char *rv = NULL;
  char const *request_prefix = "http://is.gd/api.php?longurl=";
  char *request;

  size_t write_data (void *buffer, size_t size, size_t nmemb, void *userp)
  {
    size_t buffer_size = size * nmemb;
    size_t new_size = rv_size + buffer_size;
    rv = realloc (rv, new_size);
    memcpy (rv + rv_size, buffer, buffer_size);
    rv_size = new_size;
    return buffer_size;
  }

  request = (char *) malloc (strlen (request_prefix) + strlen (url) + 1);
  if (request == NULL)
    goto malloc_fail;

  strcpy (request, request_prefix);
  strcat (request, url);
  curl = curl_easy_init ();
  if (!curl)
    {
      goto curl_easy_init_fail;
    }

  if (curl_easy_setopt (curl, CURLOPT_URL, request) != CURLE_OK)
    goto curl_easy_setopt_fail;

  if (curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_data) != CURLE_OK)
    goto curl_easy_setopt_fail;
  res = curl_easy_perform (curl);
  if (res != CURLE_OK)
    goto curl_easy_perform_fail;

  if (curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &rc) != CURLE_OK)
    goto curl_easy_getinfo_fail;

  if (rc != HTTP_OK)
    goto response_code_not_ok;

  if (rv == NULL)
    goto no_rv;

  if (rv[rv_size - 1] != 0)
    {
      rv = realloc (rv, rv_size + 1);
      rv[rv_size] = 0;
    }

  free (request);
  return rv;

no_rv:
response_code_not_ok:
curl_easy_getinfo_fail:
curl_easy_perform_fail:
  free (rv);
  rv = NULL;
curl_easy_setopt_fail:curl_easy_cleanup (curl);
curl_easy_init_fail:
  free (request);
malloc_fail:return rv;
}


void
twapi_init ()
{
  LIBXML_TEST_VERSION curl_global_init (CURL_GLOBAL_ALL);
  xmlInitParser ();
}

void
twapi_cleanup ()
{
  xmlCleanupParser ();
  curl_global_cleanup ();
}
